bare-module-resolve
Low-level module resolution algorithm for Bare. The algorithm is implemented as a generator function that yields either package manifests to be read or resolution candidates to be tested by the caller. As a convenience, the main export is a synchronous and asynchronous iterable that relies on package manifests being read by a callback. For asynchronous iteration, the callback may return promises which will be awaited before being passed to the generator.
npm i bare-module-resolve
Usage
For synchronous resolution:
const resolve = require('bare-module-resolve')
function readPackage(url) {
}
for (const resolution of resolve(
'./file.js',
new URL('file:///directory/'),
readPackage
)) {
console.log(resolution)
}
For asynchronous resolution:
const resolve = require('bare-module-resolve')
async function readPackage(url) {
}
for await (const resolution of resolve(
'./file.js',
new URL('file:///directory/'),
readPackage
)) {
console.log(resolution)
}
API
const resolver = resolve(specifier, parentURL[, options][, readPackage])
Resolve specifier
relative to parentURL
, which must be a WHATWG URL
instance. readPackage
is called with a URL
instance for every package manifest to be read and must either return the parsed JSON package manifest, if it exists, or null
. If readPackage
returns a promise, synchronous iteration is not supported.
Options include:
options = {
imports,
builtins: [],
builtinProtocol: 'builtin:',
conditions: [],
matchedConditions: [],
engines: {},
extensions: [],
resolutions
}
for (const resolution of resolver)
Synchronously iterate the module resolution candidates. The resolved module is the first candidate that exists, either as a file on a file system, a resource at a URL, or something else entirely.
for await (const resolution of resolver)
Asynchronously iterate the module resolution candidates. If readPackage
returns promises, these will be awaited. The same comments as for (const resolution of resolver)
apply.
Algorithm
The following generator functions implement the resolution algorithm, which has been adapted from the Node.js resolution algorithms for CommonJS and ES modules. Unlike Node.js, Bare uses the same resolution algorithm for both module formats. The yielded values have the following shape:
Package manifest
next.value = {
package: URL
}
If the package manifest identified by next.value.package
exists, generator.next()
must be passed the parsed JSON value of the manifest. If it does not exist, pass null
instead.
Resolution candidate
next.value = {
resolution: URL
}
If the module identified by next.value.resolution
exists, generator.next()
may be passed true
to signal that the resolution for the current set of conditions has been identified. If it does not exist, pass false
instead.
To drive the generator functions, a loop like the following can be used:
const generator = resolve.module(specifier, parentURL)
let next = generator.next()
while (next.done !== true) {
const value = next.value
if (value.package) {
let info
next = generator.next(info)
} else {
const resolution = value.resolution
let resolved
next = generator.next(resolved)
}
}
Options are the same as resolve()
for all functions.
[!WARNING]
These functions are currently subject to change between minor releases. If using them directly, make sure to specify a tilde range (~1.2.3
) when declaring the module dependency.
const generator = resolve.module(specifier, parentURL[, options])
- If
specifier
starts with a Windows drive letter:
- Prepend
/
to specifier
.
- If
options.resolutions
is set:
- If
preresolved(specifier, options.resolutions, parentURL, options)
yields, return.
- If
url(specifier, parentURL, options)
yields, return. - If
packageImports(specifier, parentURL, options)
yields, return. - If
specifier
equals .
or ..
, or if specifier
starts with /
, \
, ./
, .\
, ../
, or ..\
:
- If
options.imports
is set:
- If
packageImportsExports(specifier, options.imports, parentURL, true, options)
yields, return.
- If
file(specifier, parentURL, false, options)
resolves, return. - Return
directory(specifier, parentURL, options)
.
- Return
package(specifier, parentURL, options)
.
const generator = resolve.url(url, parentURL[, options])
- If
url
is not a valid URL, return. - If
options.imports
is set:
- If
packageImportsExports(url.href, options.imports, parentURL, true, options)
yields, return.
- If
url.protocol
equals node:
:
- Let
specifier
be url.pathname
. - If
specifier
equals .
or ..
, or if specifier
starts with /
, \
, ./
, .\
, ../
, or ..\
, throw. - Return
package(specifier, parentURL, options)
.
- Yield
url
.
const generator = resolve.preresolved(specifier, resolutions, parentURL[, options])
- Let
imports
be resolutions[parentURL]
. - If
imports
is a non-null
object:
- Return
packageImportsExports(specifier, imports, parentURL, true, options)
.
const generator = resolve.package(packageSpecifier, parentURL[, options])
- If
packageSpecifier
is the empty string, throw. - If
packageSpecifier
does not start with @
:
- Set
packageName
to the substring of packageSpecifier
until the first /
or the end of the string.
- Let
packageName
be undefined
. - Otherwise:
- If
packageSpecifier
does not include /
, throw. - Set
packageName
to the substring of packageSpecifier
until the second /
or the end of the string.
- If
packageName
starts with .
or includes \
or %
, throw. - If
builtinTarget(packageSpecifier, null, options.builtins, options)
yields, return. - Let
packageSubpath
be .
concatenated with the substring of packageSpecifier
from the position at the length of packageName
. - If
packageSelf(packageName, packageSubpath, parentURL, options)
yields, return. - Repeat:
- Let
packageURL
be the resolution of node_modules/
concatenated with packageName
and /
relative to parentURL
. - Set
parentURL
to the substring of parentURL
until the last /
. - Let
info
be the result of yielding the resolution of package.json
relative to packageURL
. - If
info
is not null
:
- If
info.engines
is set:
- Call
validateEngines(packageURL, info.engines, options)
.
- If
info.exports
is set:
- Return
packageExports(packageURL, packageSubpath, info.exports, options)
.
- If
packageSubpath
is .
:
- If
info.main
is a non-empty string:
- Set
packageSubpath
to info.main
.
- Otherwise:
- Return
file('index', packageURL, true, options)
.
- If
file(packageSubpath, packageURL, false, options)
resolves, return. - Return
directory(packageSubpath, packageURL, options)
.
- If
parentURL
is the file system root, return.
const generator = resolve.packageSelf(packageName, packageSubpath, parentURL[, options])
- For each value
packageURL
of lookupPackageScope(parentURL, options)
:
- Let
info
be the result of yielding packageURL
. - If
info
is not null
:
- If
info.name
does not equal packageName
, return. - If
info.exports
is set:
- Return
packageExports(packageURL, packageSubpath, info.exports, options)
.
- If
packageSubpath
is .
:
- If
info.main
is a non-empty string:
- Set
packageSubpath
to info.main
.
- Otherwise:
- Return
file('index', packageURL, true, options)
.
- If
file(packageSubpath, packageURL, false, options)
resolves, return. - Return
directory(packageSubpath, packageURL, options)
.
const generator = resolve.packageExports(packageURL, subpath, exports[, options])
- If
subpath
is .
:
- Let
mainExport
be undefined
. - If
exports
is a string or an array:
- Set
mainExport
to exports
.
- If
exports
is a non-null
object:
- If some keys of
exports
start with .
:
- If
.
is a key of exports
:
- Set
mainExport
to exports['.']
.
- Otherwise:
- Set
mainExport
to exports
.
- If
mainExport
is not undefined
:
- If
packageTarget(packageURL, mainExport, null, false, options)
yields, return.
- Otherwise, if
exports
is a non-null
object:
- If every key of
exports
starts with .
:
- If
packageImportsExports(subpath, exports, packageURL, false, options)
yields, return.
- Throw.
const generator = resolve.packageImports(specifier, parentURL[, options])
- If
specifier
is #
or starts with #/
, throw. - For each value
packageURL
of lookupPackageScope(parentURL, opions)
:
- Let
info
be the result of yielding packageURL
. - If
info
is not null
:
- If
info.imports
is set:
- If
packageImportsExports(specifier, info.imports, packageURL, true, options)
yields, return.
- If specifier starts with
#
, throw. - Return.
- If
options.imports
is set:
- If
packageImportsExports(url.href, options.imports, parentURL, true, options)
yields, return.
const generator = resolve.packageImportsExports(matchKey, matchObject, packageURL, isImports[, options])
- If
matchKey
is a key of matchObject
and matchKey
does not include *
:
- Let
target
be matchObject[matchKey]
. - Return
packageTarget(packageURL, target, null, isImports, options)
.
- Let
expansionKeys
be the keys of matchObject
that include *
sorted by patternKeyCompare
. - For each value
expansionKey
of expansionKeys
:
- Let
patternBase
be the substring of expansionKey
until the first *
. - If
matchKey
starts with but isn't equal to patternBase
:
- Let
patternTrailer
be the substring of expansionKey
from the position at the index after the first *
. - If
patternTrailer
is the empty string, or if matchKey
ends with patternTrailer
and the length of matchKey
is greater than or equal to the length of expansionKey
:
- Let
target
be matchObject[expansionKey]
. - Let
patternMatch
be the substring of matchKey
from the position at the length of patternBase
until the length of matchKey
minus the length of patternTrailer
. - Return
packageTarget(packageURL, target, patternMatch, isImports, options)
.
const generator = resolve.packageTarget(packageURL, target, patternMatch, isImports[, options])
- If
target
is a string:
- If
target
does not start with ./
and isImports
is false
, throw. - If
patternMatch
is not null
:
- Replace every instance of
*
in target
with patternMatch
.
- If
url(target, packageURL, options)
yields, return. - If
target
equals .
or ..
, or if target
starts with /
, ./
, or ../
:
- Yield the resolution of
target
relative to packageURL
and return.
- Return
package(target, packageURL, options)
.
- If
target
is an array:
- For each value
targetValue
of target
:
- If
packageTarget(packageURL, targetValue, patternMatch, isImports, options)
yields, return.
- If
target
is a non-null
object:
- For each key
condition
of target
:
- If
condition
equals default
or if options.conditions
includes condition
:
- Let
targetValue
be target[condition]
. - Return
packageTarget(packageURL, targetValue, patternMatch, isImports, options)
.
const generator = resolve.builtinTarget(packageSpecifier, packageVersion, target[, options])
- If
target
is a string:
- If
target
does not start with @
:
- Let
targetName
be the substring of target
until the first @
or the end of the string. - Let
targetVersion
be the substring of target
from the character following the first @
and to the end of string, or null
if no such substring exists.
- Otherwise:
- Let
targetName
be the substring of target
until the second @
or the end of the string. - Let
targetVersion
be the substring of target
from the character following the second @
and to the end of string, or null
if no such substring exists.
- If
packageSpecifier
equals targetName
:
- If
packageVersion
is null
and targetVersion
is null
:
- Yield
options.builtinProtocol
concatenated with packageSpecifier
and return.
- Let
version
be null
. - If
packageVersion
is null
, let version
be targetVersion
. - Otherwise, if
targetVersion
is either null
or equals packageVersion
, let version
be packageVersion
- If
version
is not null
:
- Yield
options.builtinProtocol
concatenated with packageSpecifier
, @
, and version
and return.
- If
target
is an array:
- For each value
targetValue
of target
:
- If
builtinTarget(packageSpecifier, packageVersion, targetValue, options)
yields, return.
- If
target
is a non-null
object:
- For each key
condition
of target
:
- If
condition
equals default
or if options.conditions
includes condition
:
- Let
targetValue
be target[condition]
. - Return
builtinTarget(packageSpecifier, packageVersion, targetValue, options)
.
const generator = resolve.file(filename, parentURL, isIndex[, options])
- If
filename
equals .
or ..
, or if filename
ends with /
or \
, return. - If
parentURL
is a file:
URL and filename
includes encoded /
or \
, throw. - If
isIndex
is false
:
- Yield the resolution of
filename
relative to parentURL
.
- For each value
ext
of options.extensions
:
- Yield the resolution of
filename
concatenated with ext
relative to parentURL
.
const generator = resolve.directory(dirname, parentURL[, options])
- Let
directoryURL
be undefined
. - If
dirname
ends with /
or \
:
- Set
directoryURL
to the resolution of dirname
relative to parentURL
.
- Otherwise:
- Set
directoryURL
to the resolution of dirname
concatenated with /
relative to parentURL
.
- Let
info
be the result of yielding the resolution of package.json
relative to directoryURL
. - If
info
is not null
:
- If
info.exports
is set:
- Return
packageExports(directoryURL, '.', info.exports, options)
.
- If
info.main
is a non-empty string:
- If
file(info.main, directoryURL, false, options)
resolves, return. - Return
directory(info.main, directoryURL, options)
.
- Return
file('index', directoryURL, true, options)
.
License
Apache-2.0